/**
 *  @file XmlWriter.cpp
 */
#include "XmlWriter.h"
#include "../../common/CppMemPool.h"
#include "../../common/UtilFuncT.h"
#include "../../exceptions/OutOfBoundsException.h"
#include "../../convert/Base64.h"
#include "../../text/UTFCharConv.h"
#include "XmlWriterException.h"

using namespace cppflib::io;
using namespace cppflib::collections;
using namespace cppflib::lang;
using namespace cppflib::exceptions;
using namespace cppflib::convert;
using namespace cppflib::util;

namespace cppflib
{

namespace xml
{

namespace writer
{

/**
 *  state of a XmlWriter
 */
typedef enum _writerstate
{
   WS_UNKNOWN = 0x01,

   WS_DOCSTART = 0x02,         ///< document start
   WS_STARTELEMENT = 0x04,     ///< write start of element
   WS_ATTR = 0x08,             ///< write a name-value attribute
   WS_CONTENT = 0x10,          ///< write element content
   WS_ENDELEMENT = 0x20,       ///< write end of element
   WS_COMMENT = 0x40,          ///< write comment
   WS_DOCEND = 0x80,
} writerstate_t;

//! Key to access the temp chars buffer at pTempEscapedChars
PRIVATE const wc_t TEMPCHARBUFFER_KEY = 0xffff;

XmlWriter::XmlWriter(void)
{
   this->pTagNameStack = NULL;
   this->pAttrTempTable = NULL;
   this->pTempEscapedChars = NULL;
   this->pOutputWriter = NULL;
   this->pWriterState = NULL;
}

/**
 *  @param [in] pOutputWriter -- the output writer to write
 */
XmlWriter::XmlWriter(Writer * pOutputWriter)
{
   this->writerSettings = XmlWriter::GetDefaultXmlWriterSettings(); // use default settings
   InternalInit(pOutputWriter);
}

/**
 *  @param [in] pOutputWriter -- the output writer to write
 *  @param [in] writerSettings -- user provided settings
 */
XmlWriter::XmlWriter(Writer * pOutputWriter, xmlwritersettings_t &writerSettings)
{
   this->writerSettings = writerSettings;
   InternalInit(pOutputWriter);
}

void XmlWriter::InternalInit(Writer * pOutputWriter)
{
   const int TEMPBUFSIZE = 128;

   this->pTagNameStack = new StackCppObjT<CFString>(false);
   cppmempool::Retain(this->pTagNameStack);
   this->pAttrTempTable = new HashtableT<CFString, Boolean>();
   cppmempool::Retain(this->pAttrTempTable);
   this->pOutputWriter = pOutputWriter;
   cppmempool::Retain(this->pOutputWriter);
   this->pWriterState = new BitAttributeT<enumforward_t>(); // actually writerstate_t
   cppmempool::Retain(this->pWriterState);
   this->pWriterState->Replace(WS_DOCSTART);

   this->pTempEscapedChars = new HashtableT<WChar, CFString>();
   cppmempool::Retain(this->pTempEscapedChars);
   this->pTempEscapedChars->Set(new WChar('&'), new CFString("&amp;"));
   this->pTempEscapedChars->Set(new WChar('<'), new CFString("&lt;"));
   this->pTempEscapedChars->Set(new WChar('>'), new CFString("&gt;"));
   this->pTempEscapedChars->Set(new WChar('\''), new CFString("&apos;"));
   this->pTempEscapedChars->Set(new WChar('"'), new CFString("&quot;"));
   // abuse the usage of pTempEscapedChars which holds a buffer for temp chars
   this->pTempEscapedChars->Set(new WChar(TEMPCHARBUFFER_KEY), new CFString("", 0, TEMPBUFSIZE)); 
   
}

XmlWriter::~XmlWriter(void)
{
   Close();
}

void XmlWriter::Close()
{
   if (pTagNameStack) {
      cppmempool::Release(pTagNameStack);
      pTagNameStack = NULL;
   }

   if (pAttrTempTable) {
      cppmempool::Release(pAttrTempTable);
      pAttrTempTable = NULL;
   }

   if (pOutputWriter) {
      if (writerSettings.closeOutputWriter)
         pOutputWriter->Close();
      else 
         pOutputWriter->Flush(); // writer does not close; make a flush here
      cppmempool::Release(pOutputWriter);
      pOutputWriter = NULL;
   }

   if (pTempEscapedChars) {
      cppmempool::Release(pTempEscapedChars);
      pTempEscapedChars = NULL;
   }

   if (pWriterState) {
      cppmempool::Release(pWriterState);
      pWriterState = NULL;
   }
}

void XmlWriter::Flush()
{
   pOutputWriter->Flush();
}

/**
 *  Write new line if 'indent' is enabled
 */
void XmlWriter::WriteNewline()
{
   if (writerSettings.indent) {
      if (writerSettings.newLineChars == NLCH_LF) {
         pOutputWriter->Write(_S('\n'));
      }
      else if (writerSettings.newLineChars == NLCH_CRLF) {
         pOutputWriter->Write(_S('\r'));
         pOutputWriter->Write(_S('\n'));
      }
   }
}

/**
 *  Write indenting spaces if 'indent' is enabled
 */
void XmlWriter::WriteIndentSpaces(bool isEndElement)
{
   if (writerSettings.indent) {
      int indentNum = static_cast<int>(pTagNameStack->GetCount());
      if (isEndElement) --indentNum;  // ending tag should be one less
        
      for (int i = 0; i < indentNum; ++i) {
         if (writerSettings.indentChars == INCH_1_SP) {
            pOutputWriter->Write(_S(' '));
         }
         else if (writerSettings.indentChars == INCH_2_SP) {
            pOutputWriter->Write(_S(' '));
            pOutputWriter->Write(_S(' '));
         }
         else if (writerSettings.indentChars == INCH_3_SP) {
            pOutputWriter->Write(_S(' '));
            pOutputWriter->Write(_S(' '));
            pOutputWriter->Write(_S(' '));
         }
         else if (writerSettings.indentChars == INCH_1_TAB) {
            pOutputWriter->Write(_S('\t'));
         }

      }
   }
}

void XmlWriter::WriteXmlDeclarationLine()
{
   if (this->pWriterState->IsAttrSet(WS_DOCSTART)) {
      // write XML declaration line
      if (this->writerSettings.includeXmlDeclaration) {
         CFString s(static_cast<pcwstr_t>(NULL), 0, 64);
         s.Format(_S("<?xml version=\"1.0\" encoding=\"%s\"?>"), 
                             (this->writerSettings.charSet == text::CS_UTF8) ? _S("utf-8") : _S("utf-16"));
         pOutputWriter->Write(s);
         WriteNewline();
      }
   }
}

/**
 *  Check if it is a valid tag/attribute name. Throw exception if invalid
 */
PRIVATE void __CheckTagAttrName(const CFString &tagAttrName)
{
   if (tagAttrName.IsEmpty())
      throw XmlWriterException(_S("Can't write empty name"));

   // first character
   wc_t c = tagAttrName[0];
   if (!(c == _S('_') || (c >= _S('A') && c <= _S('Z')) || (c >= _S('a') && c <= _S('z')) || c >= 0xff)) {
      throw XmlWriterException(_S("Name starts with an illegal character"));
   }

   // other characters
   for (int i = 1; i < tagAttrName.GetLength(); ++i) {
      c = tagAttrName[i];
      if (c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || 
          c == '.' || c == '-' || c == ':' || (c >= 0xff)) {  // tag name may have non-English characters
         // nothing
      }
      else {
         throw XmlWriterException(_S("Name contains illegal character(s)"));
      }
   }
}

/**
 *  To check if state of the writer is legal or not
 *
 *  @param [in] nextState -- next expected state
 */
PRIVATE void __CheckWriterState(BitAttributeT<enumforward_t> &curState, writerstate_t nextState)
{
      if (curState.IsAttrSet(WS_DOCSTART)) {
         if (nextState != WS_STARTELEMENT)
            throw XmlWriterException(_S("Illegal call after starting a XML document"));
      }
      else if (curState.IsAttrSet(WS_STARTELEMENT)) {
         if (nextState == WS_UNKNOWN || nextState == WS_DOCSTART || nextState == WS_DOCEND)
            throw XmlWriterException(_S("Illegal call after starting an element"));
      }
      else if (curState.IsAttrSet(WS_ATTR)) {
         if (nextState == WS_UNKNOWN || nextState == WS_DOCSTART || nextState == WS_DOCEND)
            throw XmlWriterException(_S("Illegal call after writing an attribute"));
      }
      else if (curState.IsAttrSet(WS_CONTENT) || curState.IsAttrSet(WS_COMMENT)) {
         if (nextState == WS_UNKNOWN || nextState == WS_DOCSTART ||
             nextState == WS_ATTR || nextState == WS_DOCEND)
            throw XmlWriterException(_S("Illegal call after writing element content"));
      }
      else if (curState.IsAttrSet(WS_ENDELEMENT)) {
         if (nextState == WS_UNKNOWN || nextState == WS_DOCSTART ||
             nextState == WS_ATTR || nextState == WS_DOCEND)
            throw XmlWriterException(_S("Illegal call after ending an element"));
      }
      else if (curState.IsAttrSet(WS_DOCEND)) {
         throw XmlWriterException(_S("Illegal call after ending the root element"));
      }
      else {
         throw XmlWriterException(_S("XmlWriter in unknown state"));
      }
}

/**
 *  Close the starting element if not yet
 */
void XmlWriter::WriteClosingStartElement()
{
   if (this->pWriterState->IsAttrSet(WS_STARTELEMENT) || this->pWriterState->IsAttrSet(WS_ATTR))
      pOutputWriter->Write(_S('>')); // close starting element first
}


PRIVATE void __FlushBufferIfFull(CFString &strBuf, Writer &w, bool forceFlush)
{
   if ((forceFlush && strBuf.GetLength() > 0) || 
        strBuf.GetLength() == strBuf.GetSize() - 1) {  // (size - 1) because string has a '\0' at the end
      w.Write(strBuf, 0, strBuf.GetLength());
      strBuf = _S(""); // make it empty
   }
}

/**
 *  This will escape characters inside 'str' if it contains illegal XML characters (i.e. '&', '<', '>', ''', '"')
 */
void XmlWriter::WriteStringValue(pcwstr_t str, int offset, int count)
{
   int end = offset + count;
   WChar ch(TEMPCHARBUFFER_KEY);
   CFString &strBuf = *pTempEscapedChars->Get(ch);  // temp string buffer

   strBuf = _S(""); // empty first

   for (int i = offset; i < end; ++i) {
      wc_t c = str[i];

      if (text::IsUTF16HighSurrogate(c)) { // surrogate pairs
         if (i + 1 >= end)
            continue;
         
         wc_t c2 = str[++i];
         if (!text::IsUTF16LowSurrogate(c2)) 
            continue;
         
         __FlushBufferIfFull(strBuf, *pOutputWriter, true); // flush previous

         strBuf = c; // high surrogate
         strBuf += c2; // low surrogate
         __FlushBufferIfFull(strBuf, *pOutputWriter, true); // flush surrogate pairs
      }
      else {
         ch.Set(c);

         if (pTempEscapedChars->ContainsKey(ch)) {
            __FlushBufferIfFull(strBuf, *pOutputWriter, true); // flush previous
            pOutputWriter->Write(*pTempEscapedChars->Get(ch));
         }
         else {
            __FlushBufferIfFull(strBuf, *pOutputWriter, false);
            strBuf += c;
         }
      }
   }

   // flush remaining chars if any
   __FlushBufferIfFull(strBuf, *pOutputWriter, true);
}

/**
 *  Write white spaces
 *
 *  @param [in] numWS -- number of white space (i.e. ASCII 0x20)
 */
void XmlWriter::WriteWhitespaces(u32_t numWS)
{
   for (u32_t i = 0 ; i < numWS; ++i)
      pOutputWriter->Write(_S(' '));
}


/**
 *  Write start of an element with a tag name
 */
XmlWriter & XmlWriter::WriteStartElement(const CFString &localName)
{
   __CheckWriterState(*pWriterState, WS_STARTELEMENT);
   __CheckTagAttrName(localName);

   WriteXmlDeclarationLine();
   WriteClosingStartElement();
   if (this->pWriterState->IsAttrSet(WS_STARTELEMENT) || this->pWriterState->IsAttrSet(WS_ATTR)) // previous write is a 'start element'
      WriteNewline();
   WriteIndentSpaces();

   // write tag
   pOutputWriter->Write(_S('<'));
   pOutputWriter->Write(localName);

   // update state
   this->pTagNameStack->Push(new CFString(localName));
   this->pAttrTempTable->Clear();  // clear attribute table to prepare for new attributes
   this->pWriterState->Replace(WS_STARTELEMENT);

   return *this;
}


/**
 *  Write attribute name-value pairs
 */
XmlWriter & XmlWriter::WriteAttribute(const CFString &attrName, const CFString &attrValue)
{
   __CheckWriterState(*pWriterState, WS_ATTR);
   __CheckTagAttrName(attrName);
   if (pAttrTempTable->ContainsKey(attrName))
      throw XmlWriterException(CFString::SFormat(_S("Duplicated attribute name -- %s"), static_cast<pcwstr_t>(attrName)));

   WriteWhitespaces(1);
   pOutputWriter->Write(attrName); // write name
   pOutputWriter->Write(_S('='));
   pOutputWriter->Write(_S('"'));
   WriteStringValue(static_cast<pcwstr_t>(attrValue), 0, attrValue.GetLength());    // write value
   pOutputWriter->Write(_S('"'));

   // update state
   this->pAttrTempTable->Set(new CFString(attrName), new Boolean(true));
   this->pWriterState->Replace(WS_ATTR);

   return *this;
}

/**
 *  Write element text content
 */
XmlWriter & XmlWriter::WriteElementContent(const CFString &content)
{
   return WriteElementContent(content, 0, content.GetLength());
}

/**
 *  Write element text content
 */
XmlWriter & XmlWriter::WriteElementContent(const CFString &content, int offset, int count)
{
   __CheckWriterState(*pWriterState, WS_CONTENT);
   
   if (count < 0 || offset < 0 || offset >= content.GetLength())
      throw OutOfBoundsException(_S("Writing content out of its size"));

   // use the smaller one
   count = cppflib::Min<int>(count, content.GetLength() - offset);

   WriteClosingStartElement(); // close starting element first
   WriteStringValue(static_cast<pcwstr_t>(content), offset, count);

   // update state
   this->pWriterState->Replace(WS_CONTENT);

   return *this;
}

/**
 *  Write element text content
 */
XmlWriter & XmlWriter::WriteElementContent(ArrayWChar &content)
{
   return WriteElementContent(content, 0, static_cast<int>(content.GetSize()));
}

/**
 *  Write element text content
 */
XmlWriter & XmlWriter::WriteElementContent(ArrayWChar &content, int offset, int count)
{
   __CheckWriterState(*pWriterState, WS_CONTENT);
   
   if (count < 0 || offset < 0 || offset >= static_cast<int>(content.GetSize()))
      throw OutOfBoundsException(_S("Writing content out of its size"));

   // use the smaller one
   count = cppflib::Min<int>(count, content.GetSize() - offset);

   WriteClosingStartElement(); // close starting element first
   WriteStringValue(content.GetRawPtr(), offset, count);

   // update state
   this->pWriterState->Replace(WS_CONTENT);

   return *this;
}

/**
 *  Write end of element. If no element content is written, a short tag end "/>" is written, otherwise,
 *  a full tag end is written
 */
XmlWriter & XmlWriter::WriteEndElement()
{
   __CheckWriterState(*pWriterState, WS_ENDELEMENT);

   if (this->pTagNameStack->GetCount() == 0)
      throw XmlWriterException(_S("No more element to end!"));

   // previous write is a 'end element' or a comment
   if (this->pWriterState->IsAttrSet(WS_ENDELEMENT) || this->pWriterState->IsAttrSet(WS_COMMENT)) 
      WriteIndentSpaces(true);

   if (this->pWriterState->IsAttrSet(WS_STARTELEMENT) || this->pWriterState->IsAttrSet(WS_ATTR)) {
      pOutputWriter->Write(_S('/'));
      pOutputWriter->Write(_S('>'));
   }
   else {
      // write ending tag
      pOutputWriter->Write(_S('<'));  
      pOutputWriter->Write(_S('/'));
      pOutputWriter->Write(*pTagNameStack->Peek());
      pOutputWriter->Write(_S('>'));
   }

   WriteNewline();

   // update
   if (pTagNameStack->PopAndRemove() == 0)
      this->pWriterState->Replace(WS_DOCEND);
   else
      this->pWriterState->Replace(WS_ENDELEMENT);

   return *this;
}


/**
 *  This will always write a full tag end (i.e. "</xxxx>")
 */
XmlWriter & XmlWriter::WriteFullEndElement()
{
   __CheckWriterState(*pWriterState, WS_ENDELEMENT);

   if (this->pTagNameStack->GetCount() == 0)
      throw XmlWriterException(_S("No more element to end!"));

   if (this->pWriterState->IsAttrSet(WS_ENDELEMENT)) // previous write is also 'end element'
      WriteIndentSpaces(true);
   else
      WriteClosingStartElement();  // close starting element first

   // write ending tag
   pOutputWriter->Write(_S('<'));  
   pOutputWriter->Write(_S('/'));
   pOutputWriter->Write(*pTagNameStack->Peek());
   pOutputWriter->Write(_S('>'));   

   WriteNewline();

   // update
   if (pTagNameStack->PopAndRemove() == 0)
      this->pWriterState->Replace(WS_DOCEND);
   else
      this->pWriterState->Replace(WS_ENDELEMENT);

   return *this;
}

/**
 *  A shortcut to write a simple element with a tag and a value only 
 */
XmlWriter & XmlWriter::WriteElement(const CFString &localName, const CFString &content)
{
   WriteStartElement(localName);
   WriteElementContent(content);
   WriteEndElement();

   return *this;
}

/**
 *  Write content using base64 encoding
 */
XmlWriter & XmlWriter::WriteBase64(ArrayByte &buffer, int offset, int count)
{
   __CheckWriterState(*pWriterState, WS_CONTENT);

   if (count < 0 || offset < 0 || offset >= static_cast<int>(buffer.GetSize()))
      throw OutOfBoundsException(_S("Writing buffer out of its size"));

   // use the smaller one
   count = cppflib::Min<int>(count, buffer.GetSize() - offset);

   WriteClosingStartElement(); // close starting element first

   const u32_t SRCBUFSIZE = 3;
   const u32_t OUTBUFSIZE = 4;
   
   ArrayWChar tempArray(OUTBUFSIZE);
   u8_t outBuf[OUTBUFSIZE];
   u8_t *pSrcBuf = buffer.GetRawPtr() + offset, *pOutBuf = static_cast<u8_t*>(outBuf);
   u8_t *pBufEnd = pSrcBuf + count;
   int numByte = 0, outLen = 0;

   while (pSrcBuf < pBufEnd) { 
      // encode 3 src bytes to 4 output bytes each time
      numByte = cppflib::Min<int>(static_cast<ptr_t>(pBufEnd - pSrcBuf), SRCBUFSIZE);
      outLen = OUTBUFSIZE;
      base64::Encode(pSrcBuf, numByte, &pOutBuf, &outLen);  // base64 encode

      for (int j = 0; j < outLen; ++j)
         tempArray[j] = static_cast<wc_t>(pOutBuf[j]);  // base64'd characters are ASCII

      pOutputWriter->Write(tempArray, 0, outLen);

      pSrcBuf += numByte;
   }

   // update state
   this->pWriterState->Replace(WS_CONTENT);

   return *this;
}

/**
 *  Write content embedded inside a CDATA. No characters inside 'text' will be escaped.
 */
XmlWriter & XmlWriter::WriteCDATA(const CFString &text)
{
   __CheckWriterState(*pWriterState, WS_CONTENT);

   if (text.Find(_S("]]>")) >= 0)
      throw XmlWriterException(_S("String inside CDATA cannot contain \"]]>\""));

   WriteClosingStartElement(); // close starting element first   

   // write <![CDATA[
   pOutputWriter->Write(_S('<'));
   pOutputWriter->Write(_S('!'));
   pOutputWriter->Write(_S('['));
   pOutputWriter->Write(_S('C'));
   pOutputWriter->Write(_S('D'));
   pOutputWriter->Write(_S('A'));
   pOutputWriter->Write(_S('T'));
   pOutputWriter->Write(_S('A'));
   pOutputWriter->Write(_S('['));

   pOutputWriter->Write(text);

   // write ]]>
   pOutputWriter->Write(_S(']'));
   pOutputWriter->Write(_S(']'));
   pOutputWriter->Write(_S('>'));

   // update state
   this->pWriterState->Replace(WS_CONTENT);

   return *this;
}

/**
 *  Write XML comment
 */
XmlWriter & XmlWriter::WriteComment(const CFString &comment)
{
   __CheckWriterState(*pWriterState, WS_COMMENT);

   if (comment.Find(_S("--")) >= 0)
      throw XmlWriterException(_S("String inside comment cannot contain \"--\""));

   WriteClosingStartElement(); // close starting element first
   if (!this->pWriterState->IsAttrSet(WS_COMMENT)) // previous write isn't a comment
      WriteNewline();
   WriteIndentSpaces();
   

   // write <!--
   pOutputWriter->Write(_S('<'));
   pOutputWriter->Write(_S('!'));
   pOutputWriter->Write(_S('-'));
   pOutputWriter->Write(_S('-'));
   pOutputWriter->Write(_S(' '));

   pOutputWriter->Write(comment);
   
   // write -->
   pOutputWriter->Write(_S(' '));
   pOutputWriter->Write(_S('-'));
   pOutputWriter->Write(_S('-'));
   pOutputWriter->Write(_S('>'));

   WriteNewline();

   // update state
   this->pWriterState->Replace(WS_COMMENT);

   return *this;
}

/**
 *  Close all elements automatically
 */
void XmlWriter::WriteEndDocument()
{
   if (this->pWriterState->IsAttrSet(WS_DOCSTART))
      return;

   while (pTagNameStack->GetCount() > 0)
      WriteEndElement();
}

/**
 *  Return a structure with default settings (on stack)
 */
xmlwritersettings_t XmlWriter::GetDefaultXmlWriterSettings()
{
   xmlwritersettings_t s;
   s.closeOutputWriter = false;
   s.charSet = cppflib::text::CS_UTF8;
   s.indent = true;
   s.indentChars = INCH_2_SP;
   s.newLineChars = NLCH_LF;
   s.includeXmlDeclaration = true;
   return s;
}



} // end of namespace writer

} // end of namespace xml

} // end of namespace cppflib
